using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Wrath.Xml
{
    public class FileFormatException : Exception
    {
        public long LineNumber;
        public string LineExcess;
        public string Line;
        public string Filename;

        public FileFormatException(string message, string filename, long lineNumber, string line, string lineExcess)
            : base(message)
        {
            Filename = filename;
            LineNumber = lineNumber;
            Line = line;
            LineExcess = lineExcess;
        }
    }

    public class FileParser
    {
        private Element rootElement;
        public Element Root { get { return rootElement; } }

        private bool modified = false;
        public bool Modified { get { return modified; } set { modified = value; } }

        private string filename;
        public string Filename { get { return filename; } }

        #region Constructors

        public FileParser()
        {
            filename = null;
            modified = false;
            rootElement = new Element();
        }

        public FileParser(string filename)
        {
            modified = false;
            ParseFile(filename);
        }

        #endregion

        #region Private Methods

        private void ParseFile(string filename)
        {
            StreamReader reader = null;
            if (File.Exists(filename))
            {
                this.filename = filename;
                reader = new StreamReader(filename);
            }
            else
            {
                // Not a valid filename, assume its inline.
                this.filename = "* INMEMORY *";                
                MemoryStream memoryXML = new MemoryStream(Encoding.ASCII.GetBytes(filename));
                reader = new StreamReader(memoryXML);
            }
            
            if (reader != null)
            {
                rootElement = new Element();
                rootElement.Name = "(root)";

                Element parentElement = rootElement;
                Element currentElement = rootElement;

                string valueBuffer = "";

                string line = "";
                long lineNumber = 0;
                bool readTextValue = false;

                bool readLine = true;
                string lineExcess = null;

                try
                {
                    while (!reader.EndOfStream)
                    {
                        if (readLine)
                        {
                            line = reader.ReadLine();
                            lineNumber++;
                        }
                        else
                        {
                            line = lineExcess;
                        }

                        if (line == null) break;

                        readLine = true;


                        if (!readTextValue)
                        {
                            // Remove whitespace and leading tabs
                            line.Trim();
                            while (line.StartsWith("\t") || line.StartsWith(" "))
                            {
                                line = line.Remove(0, 1);
                            }
                        }

                        if (line.Length > 0)
                        {
                            // Is this an element?
                            if (line.StartsWith("<") && !readTextValue)
                            {
                                string tag = line;

                                // read <?xml tags
                                if (tag.StartsWith("<?"))
                                {
                                    if (tag.IndexOf("?>") != tag.Length - 2)
                                    {
                                        // Multiple elements found on a single line.
                                        lineExcess = tag.Substring(line.IndexOf("?>") + 2);
                                        tag = tag.Substring(0, line.IndexOf("?>") + 2);

                                        readLine = false;
                                    }

                                    parentElement = currentElement;

                                    currentElement = ParseDefinition(tag);
                                    currentElement.Parent = parentElement;

                                    if (parentElement != null) // Should never happen
                                    {
                                        parentElement.ChildElements.Add(currentElement);

                                        currentElement = currentElement.Parent;
                                        parentElement = currentElement.Parent;
                                    }

                                    continue;
                                }

                                // Parse comments
                                if (tag.StartsWith("<!--"))
                                {
                                    // Handle multi-line comments
                                    while (line.IndexOf("-->") < 0)
                                    {
                                        line = reader.ReadLine();
                                        tag += "\r\n";
                                        tag += line;
                                        lineNumber++;
                                    }

                                    if (tag.IndexOf("-->") != tag.Length - 3)
                                    {
                                        // Multiple elements found on a single line.
                                        lineExcess = tag.Substring(line.IndexOf("-->") + 3);
                                        tag = tag.Substring(0, line.IndexOf("-->") + 3);

                                        readLine = false;
                                    }

                                    parentElement = currentElement;

                                    currentElement = ParseComment(tag);
                                    currentElement.Parent = parentElement;

                                    if (parentElement != null) // Should never happen
                                    {
                                        parentElement.ChildElements.Add(currentElement);

                                        currentElement = currentElement.Parent;
                                        parentElement = currentElement.Parent;
                                    }

                                    // Multiple elements found on a single line.
                                    lineExcess = line.Substring(line.IndexOf("-->") + 3);

                                    readLine = false;

                                    continue;
                                }

                                if (tag.IndexOf(">") != tag.Length - 1)
                                {
                                    // Multiple elements found on a single line.
                                    lineExcess = tag.Substring(line.IndexOf(">") + 1);
                                    tag = tag.Substring(0, line.IndexOf(">") + 1);

                                    readLine = false;
                                }

                                // Strip off < >
                                string elementText = tag.Substring(1, tag.LastIndexOf('>') - 1);

                                // Check that this element is an end element.
                                if (elementText.StartsWith("/"))
                                {
                                    if (!currentElement.IsStarted)
                                    {
                                        throw new Exception("End element without start element.");
                                    }

                                    while (valueBuffer.EndsWith("\r\n"))
                                    {
                                        valueBuffer = valueBuffer.Remove(valueBuffer.Length - 2);
                                        valueBuffer = valueBuffer.TrimEnd('\t');
                                    }

                                    currentElement.Value = valueBuffer;
                                    valueBuffer = "";
                                    readTextValue = false;

                                    currentElement.IsEnded = true;

                                    if (parentElement != null) // Should only happen with the root element.
                                    {
                                        parentElement.ChildElements.Add(currentElement);

                                        currentElement = currentElement.Parent;
                                        parentElement = currentElement.Parent;
                                    }
                                }
                                // Check if this element is a non-containing element.
                                else if (elementText.EndsWith("/"))
                                {
                                    parentElement = currentElement;
                                    currentElement = ParseStartElement(elementText);
                                    currentElement.Parent = parentElement;
                                    currentElement.IsEnded = true;

                                    parentElement.ChildElements.Add(currentElement);

                                    currentElement = currentElement.Parent;
                                    parentElement = currentElement.Parent;
                                }
                                else
                                {
                                    parentElement = currentElement;
                                    currentElement = ParseStartElement(elementText);
                                    currentElement.Parent = parentElement;

                                    // Figure out if this is a containing node or a text node????
                                    if (currentElement.Name.ToLower() == "script")
                                    {
                                        Wrath.Xml.Attribute fileAttribute = currentElement.FindAttribute("file");
                                        if (fileAttribute == null)
                                        {
                                            readTextValue = true;
                                        }
                                    }
                                    else
                                    {
                                        Wrath.Xml.Attribute typeAttribute = currentElement.FindAttribute("type");
                                        if (typeAttribute != null && typeAttribute.Value.ToLower() == "formattedtext")
                                        {
                                            readTextValue = true;
                                        }
                                    }

                                } // else                            
                            } // if is element
                            else
                            {
                                // Read until we get a closing tag...
                                if (line.IndexOf("</" + currentElement.Name + ">") > -1)
                                {
                                    lineExcess = line.Substring(line.IndexOf("</" + currentElement.Name + ">"));
                                    line = line.Substring(0, line.IndexOf("</" + currentElement.Name + ">"));

                                    readLine = false;
                                    readTextValue = false;
                                }

                                // Add arb text to valueBuffer.
                                valueBuffer += line;
                                valueBuffer += "\r\n";
                            }
                        } // if line is not blank

                    } // while

                    // Add the last element read.
                    //rootElement.ChildElements.Add(currentElement);
                }
                catch (Exception e)
                {
                    FileFormatException fileFormatException = new FileFormatException(e.Message, filename, lineNumber, line, lineExcess);
                    fileFormatException.Source = e.Source;

                    throw fileFormatException;
                }

                reader.Close();

            } // if
        }

        private Element ParseComment(string elementText)
        {
            // Expecting '<!-- ... -->'
            Element currentElement = new Element();
            currentElement.Type = ElementType.Comment;
            currentElement.Name = "";

            // Find name.
            int index = elementText.IndexOf(' ');
            if (index > 0)
            {
                int endIndex = elementText.IndexOf("-->");
                if (endIndex > 0)
                {
                    try
                    {
                        currentElement.Value = elementText.Substring(index + 1, endIndex - index - 1);
                    }
                    catch
                    {
                        // This will most likely happen if the elementText = '<-->' or similar
                        // where (endIndex - index - 1 < 0)
                        throw new Exception("Invalid comment.");
                    }
                }
            }
            else
            {
                throw new Exception("Invalid comment.");
            }

            currentElement.IsStarted = true;
            currentElement.IsEnded = true;

            return currentElement;
        }

        private Element ParseDefinition(string elementText)
        {
            // Expecting '<?xml ... ?>'
            Element currentElement = new Element();
            currentElement.Type = ElementType.Definition;
            currentElement.Name = "";

            // Find name.
            int index = elementText.IndexOf(' ');
            if (index > 0)
            {
                // Check for attributes.
                elementText = elementText.Substring(index).Trim();

                while (elementText.IndexOf('=') > -1)
                {
                    char quoteChar = '"';

                    Wrath.Xml.Attribute attribute = new Wrath.Xml.Attribute();
                    int attributeEqualsIndex = elementText.IndexOf('=');
                    int attributeOpenQuoteIndex = attributeEqualsIndex + 1;
                    while (elementText[attributeOpenQuoteIndex] != '"' && elementText[attributeOpenQuoteIndex] != '\'')
                    {
                        attributeOpenQuoteIndex++;
                        if (attributeOpenQuoteIndex > elementText.Length - 1)
                        {
                            throw new Exception("Unquoted Attribute found.");
                        }
                    }
                    quoteChar = elementText[attributeOpenQuoteIndex];
                    int attributeCloseQuoteIndex = elementText.IndexOf(quoteChar, attributeOpenQuoteIndex + 1);

                    attribute.Name = elementText.Substring(0, attributeEqualsIndex).Trim();
                    attribute.Value = elementText.Substring(attributeOpenQuoteIndex + 1, (attributeCloseQuoteIndex - attributeOpenQuoteIndex - 1));

                    currentElement.Attributes.Add(attribute);

                    elementText = elementText.Substring(attributeCloseQuoteIndex + 1).Trim();
                }
            }
            else
            {
                throw new Exception("Invalid xml definition.");
            }

            currentElement.IsStarted = true;
            currentElement.IsEnded = true;

            return currentElement;
        }

        private static Element ParseStartElement(string elementText)
        {
            Element currentElement = new Element();

            // Find name.
            int index = elementText.IndexOf(' ');
            if (index > 0)
            {
                currentElement.Name = elementText.Substring(0, index);

                // Check for attributes.
                elementText = elementText.Substring(index).Trim();

                while (elementText.IndexOf('=') > -1)
                {
                    char quoteChar = '"';

                    Wrath.Xml.Attribute attribute = new Wrath.Xml.Attribute();
                    int attributeEqualsIndex = elementText.IndexOf('=');
                    int attributeOpenQuoteIndex = attributeEqualsIndex + 1;
                    while (elementText[attributeOpenQuoteIndex] != '"' && elementText[attributeOpenQuoteIndex] != '\'')
                    {
                        attributeOpenQuoteIndex++;
                        if (attributeOpenQuoteIndex > elementText.Length - 1)
                        {
                            throw new Exception("Unquoted Attribute found.");
                        }
                    }
                    quoteChar = elementText[attributeOpenQuoteIndex];
                    int attributeCloseQuoteIndex = elementText.IndexOf(quoteChar, attributeOpenQuoteIndex + 1);

                    attribute.Name = elementText.Substring(0, attributeEqualsIndex).Trim();
                    attribute.Value = elementText.Substring(attributeOpenQuoteIndex + 1, (attributeCloseQuoteIndex - attributeOpenQuoteIndex - 1));

                    currentElement.Attributes.Add(attribute);

                    elementText = elementText.Substring(attributeCloseQuoteIndex + 1).Trim();
                }
            }
            else
            {
                currentElement.Name = elementText;
            }

            currentElement.IsStarted = true;

            return currentElement;
        }

        private bool RemoveElement(Element elementToFind, List<Element> elementsToSearch)
        {
            bool removed = false;

            foreach (Element element in elementsToSearch)
            {
                if (element == elementToFind)
                {
                    elementsToSearch.Remove(element);
                    removed = true;
                    break;
                }

                if (element.ChildElements.Count > 0)
                {
                    removed = RemoveElement(elementToFind, element.ChildElements);

                    if (removed) break;
                }
            }

            return removed;
        }

        #endregion

        #region Public Methods

        public bool Remove(Element selectedElement)
        {
            if (selectedElement == rootElement)
            {
                rootElement.ChildElements.Clear();
                return true;
            }

            return RemoveElement(selectedElement, rootElement.ChildElements);
        }

        #region Find Methods

        #region FindFirst

        public Element FindFirstElement(string elementName)
        {
            return rootElement.FindFirstElement(elementName, true);
        }

        public Element FindFirstElement(string elementName, string attributeName)
        {
            return rootElement.FindFirstElement(elementName, attributeName, true);
        }

        public Element FindFirstElement(string elementName, string attributeName, string attributeValue)
        {
            return rootElement.FindFirstElement(elementName, attributeName, attributeValue, true);
        }

        public Element FindFirstElementWithAttribute(string attributeName, string attributeValue)
        {
            return rootElement.FindFirstElementWithAttribute(attributeName, attributeValue, true);
        }

        #endregion

        #region FindAll

        public List<Element> FindAll(string elementName)
        {
            return rootElement.FindAll(elementName, true);
        }

        public List<Element> FindAll(string elementName, string attributeName)
        {
            return rootElement.FindAll(elementName, attributeName, true);
        }

        public List<Element> FindAll(string elementName, string attributeName, string attributeValue)
        {
            return rootElement.FindAll(elementName, attributeName, attributeValue, true);
        }

        public List<Element> FindAllWithAttribute(string attributeName)
        {
            return rootElement.FindAllWithAttribute(attributeName, true);
        }

        public List<Element> FindAllWithAttribute(string attributeName, string attributeValue)
        {
            return rootElement.FindAllWithAttribute(attributeName, attributeValue, true);
        }

        #endregion

        #endregion

        public void Save()
        {
            if (string.IsNullOrEmpty(filename)) throw new InvalidOperationException("No filename specified.");

            using (StreamWriter writer = new StreamWriter(filename, false))
            {
                foreach (Element element in rootElement.ChildElements)
                {
                    writer.WriteLine(element.ToString());
                }
                writer.Close();
            }
        }

        public void Save(string filename, bool overwrite)
        {
            if (!overwrite)
            {
                if (File.Exists(filename)) throw new InvalidOperationException("File already exists.");
            }

            using (StreamWriter writer = new StreamWriter(filename, false))
            {
                foreach (Element element in rootElement.ChildElements)
                {
                    writer.WriteLine(element.ToString());
                }
                writer.Close();
            }
        }

        #endregion
    }
}
